package com.netx.ebs;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletConfig;
import com.netx.generics.basic.Context;
import com.netx.generics.basic.RUN_MODE;
import com.netx.generics.basic.ErrorList;
import com.netx.generics.basic.IntegrityException;
import com.netx.generics.time.TimeValue;
import com.netx.generics.util.CountingWriter;
import com.netx.generics.util.HtmlWriter;
import com.netx.generics.util.Strings;
import com.netx.data.Connection;
import com.netx.data.DatabaseException;


public abstract class ErrorHandlerServlet extends EbsServlet {

	protected ErrorHandlerServlet() {
		super(new SecurityChecker[0]);
	}

	protected void ebsInit(ServletConfig config) throws Throwable {
		// In case an error is thrown, this Servlet keeps responding
		// because it does not check whether the application has been initialized
		// (the constructor doesn't take an ApplicationStateChecker).
		// This method may be safely overriden, since it does not provide any
		// way for the [error-handler] to become disabled.
	}

	// Callbacks:
	protected abstract void onResourceNotFound(EbsRequest request, EbsResponse response, String resourceName) throws IOException, DatabaseException;
	protected abstract void onResourceForbidden(EbsRequest request, EbsResponse response, String resourceName) throws IOException, DatabaseException;
	protected abstract void onNotAuthorized(EbsRequest request, EbsResponse response, String requestURL, Permission p) throws IOException, DatabaseException;
	protected abstract void onIllegalOperation(EbsRequest request, EbsResponse response, String requestURL, String message) throws IOException, DatabaseException;
	protected abstract void onIOException(EbsRequest request, EbsResponse response, IOException exception) throws IOException, DatabaseException;
	protected abstract void onDatabaseException(EbsRequest request, EbsResponse response, DatabaseException exception) throws IOException, DatabaseException;
	protected abstract void onUnexpectedException(EbsRequest request, EbsResponse response, Throwable exception) throws IOException, DatabaseException;
	protected abstract void onApplicationStopped(EbsRequest request, EbsResponse response, String requestURL, TimeValue timeExpected) throws IOException, DatabaseException;

	public final void doGet(EbsRequest request, EbsResponse response) {
		final Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");
		// request.getRequestURL is not useful here (it returns http://<server>/<application>/[error-handler]).
		// We can use REQUEST_ERROR_URL, if it has been set somewhere in the application
		// (for example, when throwing an IllegalArgumentException)
		final String requestURL;
		if(request.getAttribute(Constants.REQUEST_ERROR_URL) != null) {
			requestURL = (String)request.getAttribute(Constants.REQUEST_ERROR_URL);
		}
		else {
			requestURL = (String)request.getAttribute("javax.servlet.error.request_uri");
		}
		final String servletName = (String)request.getAttribute("javax.servlet.error.servlet_name");
		Throwable t = (Throwable)request.getAttribute("javax.servlet.error.exception");
		request.setAttribute(Constants.REQUEST_ERROR_URL, null);
		try {
			final EbsContext ebsCtx = getEbsContext();
			if(!ebsCtx.getSendErrorCodes()) {
				//TODO check if this really works
				response.setStatus(EbsResponse.SC_ACCEPTED);
			}
			// Treat error:
			if(statusCode == null) {
				// Direct request:
				response.sendError(EbsResponse.SC_FORBIDDEN);
			}
			else if(statusCode == EbsResponse.SC_NOT_FOUND) {
				onResourceNotFound(request, response, requestURL);
			}
			else if(statusCode == EbsResponse.SC_FORBIDDEN) {
				onResourceForbidden(request, response, requestURL);
			}
			else if(statusCode == EbsResponse.SC_INTERNAL_SERVER_ERROR) {
				// Treat exception:
				if(t instanceof SecurityCheckException) {
					if(t.getClass() == ApplicationNotInitializedException.class) {
						if(!ebsCtx.internalGetInitCalled()) {
							addInitError("fatal error: InitializerServlet not initialized (either ebsInit not called, or load-on-startup parameter not set correctly)");
						}
						// Show all init errors:
						@SuppressWarnings("unchecked")
						Map<String,List<InitError>> errorMap = (Map<String,List<InitError>>)getServletContext().getAttribute(Constants.SRVCTX_ERROR_LIST);
						_showInitErrors(errorMap, response);
					}
					else if(t.getClass() == ApplicationStoppedException.class) {
						onApplicationStopped(request, response, requestURL, ebsCtx.internalGetExpectedStartupTime());
					}
					else if(t.getClass() == NotAuthenticatedException.class) {
						response.sendRedirect(ebsCtx.getAuthenticatorName()+"?action=error&reason=no-auth");
					}
					else if(t.getClass() == SessionTerminatedException.class) {
						NotificationMessage nm = ((SessionTerminatedException)t).getNotificationMessage();
						StringBuilder url = new StringBuilder();
						url.append(ebsCtx.getAuthenticatorName());
						url.append("?action=error&reason=session-terminated&user=");
						url.append(nm.username);
						url.append("&notification=");
						url.append(nm.notification.toString().toLowerCase());
						if(nm.message != null) {
							url.append("&message=");
							url.append(nm.message);
						}
						response.sendRedirect(url.toString());
					}
					else if(t.getClass() == NotAuthorizedException.class) {
						// Save transgression:
						User u = request.getUserSession().getUser();
						String pName = ((NotAuthorizedException)t).permissionName;
						Connection c = request.getDatabaseConnection();
						Permission p = Entities.getPermissions(c).find(pName);
						Entities.getTransgressions(c).saveIllegalAccess(u.getPrimaryKey(), p, requestURL);
						c.close();
						onNotAuthorized(request, response, requestURL, p);
					}
					else if(t.getClass() == IllegalOperationException.class) {
						// Save transgression:
						Connection c = request.getDatabaseConnection();
						Entities.getTransgressions(c).saveIllegalOperation(_getUserIdFrom(request), t, requestURL);
						c.close();
						onIllegalOperation(request, response, requestURL, t.getMessage());
					}
					else {
						throw new IntegrityException(t);
					}
				}
				else {
					// Unwrap DatabaseException exceptions:
					if(t.getClass() == DatabaseExceptionWrapper.class) {
						t = t.getCause();
						// Try to close the database connection:
						try {
							Connection c = ((DatabaseException)t).getConnection();
							if(c != null) {
								c.close();
							}
						}
						catch(DatabaseException de) {
							// noop: just close if possible
						}
					}
					// Save exception in the database:
					Connection c = request.getDatabaseConnection();
					Entities.getErrors(c).save(_getUserIdFrom(request), t, requestURL);
					c.close();
					// Show message to the user:
					if(Context.getRootContext().getRunMode()==RUN_MODE.DEV && ebsCtx.getShowStackTrace()==true) {
						_showStackTrace(t, requestURL, servletName, response);
					}
					else {
						if(t instanceof IOException) {
							onIOException(request, response, (IOException)t);
						}
						else if(t instanceof DatabaseException) {
							onDatabaseException(request, response, (DatabaseException)t);
						}
						else {
							onUnexpectedException(request, response, t);
						}
					}
				}
			}
			else {
				throw new IntegrityException(statusCode);
			}
		}
		catch(Throwable t2) {
			// Internal error, not supposed to happen:
			_showCriticalException(t, requestURL, response, t2);
		}
	}

	public final void doPost(EbsRequest request, EbsResponse response) {
		doGet(request, response);
	}

	private void _showInitErrors(Map<String,List<InitError>> errorMap, EbsResponse response) throws IOException {
		// #Format:
		// [Initialization errors in <servlet>]
		// #For each error:
		//    #if only message:
		//       Message: <message>
		//    #else if ErrorList:
		//       Message: <all messages>
		//    #else:
		//       Exception: <class>
		//       Message: <message>
		//       #if DatabaseException and has query:
		//          Query: <query>
		//       Stack trace: <stack trace>
		// <br>
		response.setContentType("text/html");
		response.sendDisableCache();
		final Writer out = response.getWriter();
		final HtmlErrorPrinter printer = new HtmlErrorPrinter(response.getWriter());
		printer.printHtmlStart();
		// Iterate over init errors:
		Iterator<Map.Entry<String,List<InitError>>> it = errorMap.entrySet().iterator();
		while(it.hasNext()) {
			Map.Entry<String,List<InitError>> entry = it.next();
			printer.printHeader("[Initialization errors in "+entry.getKey()+"]");
			Iterator<InitError> itErrors = entry.getValue().iterator();
			for(int i=0; itErrors.hasNext(); i++) {
				InitError ie = itErrors.next();
				printer.printExceptionSeparator(i);
				if(ie.message != null) {
					printer.printMessage(ie.message);
				}
				else if(ie.errorList != null) {
					printer.printErrorList(ie.errorList);
				}
				else {
					printer.printException(ie.exception);
				}
				printer.printLine();
			}
		}
		printer.printHtmlEnd();
		out.close();
	}

	private void _showStackTrace(Throwable t, String requestURL, String servletName, EbsResponse response) throws IOException {
		// #Format:
		// [Error in <servlet>]
		// URL: <url> 
		// Exception: <class>
		// Message: <message>
		// #if DatabaseException and has query:
		//    Query: <query>
		// Stack trace: <stack trace>
		response.setContentType("text/html");
		response.sendDisableCache();
		final Writer out = response.getWriter();
		final HtmlErrorPrinter printer = new HtmlErrorPrinter(out);
		printer.printHtmlStart();
		printer.printHeader("[Error in "+servletName+"]");
		printer.printURL(requestURL);
		printer.printException(t);
		printer.printHtmlEnd();
		out.close();
	}
	
	private void _showCriticalException(Throwable t, String requestURL, EbsResponse response, Throwable t2) {
		getLogger().error("exception in [error-handler]: "+t2, t2);
		getLogger().error("original exception: "+t, t);
		try {
			response.setContentType("text/plain");
			response.sendDisableCache();
			final PrintWriter out = response.getWriter();
			out.print("Exception thrown in [error-handler]: ");
			out.println(t2);
			out.print("While accessing URL: ");
			out.println(requestURL);
			out.println("Stack trace:");
			t2.printStackTrace(out);
			out.print("Original exception: ");
			out.println(t);
			out.println("Stack trace:");
			t.printStackTrace(out);
			out.close();
		}
		catch(IOException io) {
			getLogger().error("exception in _showCriticalException: "+io, io);
		}
	}
	
	private long _getUserIdFrom(EbsRequest request) {
		Session s = request.getUserSession();
		if(s != null) {
			return s.getUser().getPrimaryKey();
		}
		else {
			return 0;
		}
	}
	
	private class HtmlErrorPrinter {
		private final CountingWriter _txtOut;
		private final HtmlWriter _htmlOut;
		
		public HtmlErrorPrinter(Writer out) {
			_txtOut = new CountingWriter(out);
			_htmlOut = new HtmlWriter(out);
		}

		public void printHtmlStart() {
			_txtOut.println("<html><head><title>ERROR</title></head>");
			_txtOut.println("<body bgcolor=\"#000000\"><font face=\"Verdana\" size=\"2\" color=\"#FFFFFF\">");
		}

		public void printHtmlEnd() {
			// IE browser issue: if "show friendly error pages" is set to true
			// and the sent response is <342* chars, IE will display
			// it's default error page instead of the response.
			// Here, we force the response to have at least 342* chars, to avoid this behavior.
			// *This has changed; now, we use 600 chars.
			long charsWritten = _txtOut.getWrittenCharCount() + _htmlOut.getWrittenCharCount();
			long minimumChars = 600;
			if(charsWritten < minimumChars) {
				_txtOut.println(Strings.repeat(" ", (int)(minimumChars - charsWritten)));
			}
			_txtOut.println("</font></body></html>");
		}

		public void printHeader(String header) {
			_txtOut.print("<font color=\"#FF0000\">");
			_htmlOut.print(header);
			_txtOut.println("</font>");
			_htmlOut.println();
		}

		public void printExceptionSeparator(int num) {
			_htmlOut.println("["+num+"]");
		}

		public void printMessage(String message) {
			_txtOut.print("<b>Message:</b> ");
			_htmlOut.println(message);
		}

		public void printErrorList(ErrorList el) {
			printMessage(el.toString());
		}
		
		public void printException(Throwable t) {
			_txtOut.print("<b>Exception:</b> ");
			_htmlOut.println(t);
			if(t instanceof DatabaseException) {
				String query = ((DatabaseException)t).getQuery();
				if(!Strings.isEmpty(query)) {
					_txtOut.print("<b>Query:</b> ");
					_htmlOut.println(query);
				}
			}
			_txtOut.print("<b>Stack trace:</b> ");
			t.printStackTrace(_htmlOut);
		}

		public void printURL(String url) {
			_txtOut.print("<b>URL:</b> ");
			_htmlOut.println(url);
		}
		
		public void printLine() {
			_htmlOut.println();
		}
	}
}
